Customer mindset metrics
These Python notes build on the conceptual knowledge of customer attitude metrics. We encourage the reader to review the case study provided in Chapter 6 of the book before starting the analysis below.
By the end of the application, you will be able to:
Make evidence-based marketing mix decisions by examining the diagnostic value that customer mindset metrics provide
Quantify the short- and long-run gains in sales that originate from improvement in mindset metrics.
Preparation and Set Up
Before we start, let us do some preparation work to get ready.
We need to:
Create a folder (e.g. mindset or chapter 6) and set the working directory to that folder on your device.
Download the data set needed for this Python application onto your working directory. The data set for this application, mindset_shampoo.csv is available on the website of this book.
Load the data set to Jupyter Notebbok for analysis as we proceed.
In order to load the data sets to Jupyter Notebbok, we need to specify the working directory where the dataset for this tutorial is located.
#Find the current working directory
import os
cwd = os.getcwd()
print(cwd)
#Change the working directory
os.chdir('C:/Users/gyildiri/Downloads/chapter 6')
print(os.getcwd())
C:\Users\gyildiri\Downloads\chapter 6 C:\Users\gyildiri\Downloads\chapter 6
Now we are ready to go.
1. Data
# Install the libraries
%pip install pandas statsmodels matplotlib seaborn pmdarima
Requirement already satisfied: pandas in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.1.4) Requirement already satisfied: statsmodels in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.14.0) Requirement already satisfied: matplotlib in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (3.8.0) Requirement already satisfied: seaborn in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.12.2) Requirement already satisfied: pmdarima in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.0.4) Requirement already satisfied: numpy<2,>=1.23.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (1.26.4) Requirement already satisfied: python-dateutil>=2.8.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2.8.2) Requirement already satisfied: pytz>=2020.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3.post1) Requirement already satisfied: tzdata>=2022.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3) Requirement already satisfied: scipy!=1.9.2,>=1.4 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (1.11.4) Requirement already satisfied: patsy>=0.5.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (0.5.3) Requirement already satisfied: packaging>=21.3 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (23.1) Requirement already satisfied: contourpy>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.2.0) Requirement already satisfied: cycler>=0.10 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (0.11.0) Requirement already satisfied: fonttools>=4.22.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (4.25.0) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.4.4) Requirement already satisfied: pillow>=6.2.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (10.2.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (3.0.9) Requirement already satisfied: joblib>=0.11 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (1.2.0) Requirement already satisfied: Cython!=0.29.18,!=0.29.31,>=0.29 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (3.0.10) Requirement already satisfied: scikit-learn>=0.22 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (1.2.2) Requirement already satisfied: urllib3 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (2.0.7) Requirement already satisfied: setuptools!=50.0.0,>=38.6.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (68.2.2) Requirement already satisfied: six in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from patsy>=0.5.2->statsmodels) (1.16.0) Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn>=0.22->pmdarima) (2.2.0) Note: you may need to restart the kernel to use updated packages.
import pandas as pd
import statsmodels.api as sm
import numpy as np
import statsmodels.tsa.vector_ar as vecm
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.ar_model import AutoReg, ar_select_order
from pmdarima import auto_arima
file_path = 'mindset_shampoo.csv'
# Read the CSV file into a DataFrame
df = pd.read_csv(file_path)
# Display the DataFrame
print(df.head())
time consideration liking awareness promotion advertising price \
0 1 14.260000 5.136 26.780001 2.39 449.571991 4.349
1 2 16.770000 5.010 29.930000 4.90 483.415985 4.406
2 3 17.170000 4.901 29.629999 3.80 374.109985 4.203
3 4 16.459999 5.094 27.110001 2.94 567.109985 4.145
4 5 18.430000 4.944 31.770000 2.81 418.777008 4.212
vol
0 1280.439973
1 1129.629970
2 1059.380054
3 933.610022
4 826.359987
Next, we will calculate our four criteria: potential, stickiness, and responsiveness, and conversion. Finally, by aggregating the results from all these stages we will evaluate the appeal of our marketing mix decisions.
2. Four criteria
Potential
For awareness and consideration, we need to calculate the average level of awareness and consideration for this shampoo brand and then calculate the potential by deducting one from them.
For liking, things are slightly more complicated since those values are measured on a seven-point scale. Therefore, we need to first convert those values into percentages (e.g. if liking is 3.5 out of 7, then it is actually 50 per cent of the full score and the value should be 50) and then take the average and calculate the corresponding potential.
Let’s reach the following using Python:
# Calculate potential for awareness and consideration
potential_awareness = (100 - df['awareness'].mean()) / 100
potential_consideration = (100 - df['consideration'].mean()) / 100
# Calculate potential for liking
df['liking_transformed'] = df['liking'] / 7
potential_liking = 1 - df['liking_transformed'].mean()
table_potential = pd.DataFrame({
'potential': ['awareness', 'consideration', 'liking'],
'value': [potential_awareness, potential_consideration, potential_liking]
})
print(table_potential)
potential value 0 awareness 0.725270 1 consideration 0.825208 2 liking 0.263096
We get that potential is 0.725 for awareness, 0.825 for consideration and 0.263 for liking.
Question: Judging from what you have now, which mindset metric should you put more effort into? Why?
Stickiness
Stickiness, or inertia, is the degree to which a change in the level of a metric is sustained over time, without any new stimuli. This can be modelled by a simple univariate AR(p) process on the attitude metric, where stickiness is quantified as the sum of the autoregressive coefficients. For further details, please refer to Chapter 6 of the book.
To compute stickiness, we need to run the following code in Python. It is important to note that the order of lag for these AR models is determined by model fit statistics, such as AIC or BIC.
# awareness
ar1_order = auto_arima(df['awareness'], max_order=3, information_criterion='aic')
ar1_lags = ar1_order.order[0]
# consideration
ar2_order = auto_arima(df['consideration'], max_order=3, information_criterion='aic')
ar2_lags = ar2_order.order[0]
# liking
ar3_order = auto_arima(df['liking'], max_order=3, information_criterion='aic')
ar3_lags = ar3_order.order[0]
# AutoReg modeling
result_ar1 = AutoReg(df['awareness'], lags=3).fit()
print(ar1_lags)
print(result_ar1.summary())
result_ar2 = AutoReg(df['consideration'], lags=3).fit()
print(ar2_lags)
print(result_ar2.summary())
result_ar3 = AutoReg(df['liking'], lags=3).fit()
print(result_ar3.summary())
0
AutoReg Model Results
==============================================================================
Dep. Variable: awareness No. Observations: 96
Model: AutoReg(3) Log Likelihood -230.957
Method: Conditional MLE S.D. of innovations 2.899
Date: Fri, 10 May 2024 AIC 471.915
Time: 16:05:26 BIC 484.578
Sample: 3 HQIC 477.027
96
================================================================================
coef std err z P>|z| [0.025 0.975]
--------------------------------------------------------------------------------
const 6.3947 2.548 2.510 0.012 1.401 11.389
awareness.L1 0.4521 0.100 4.528 0.000 0.256 0.648
awareness.L2 0.0458 0.110 0.417 0.677 -0.170 0.261
awareness.L3 0.2689 0.100 2.701 0.007 0.074 0.464
Roots
=============================================================================
Real Imaginary Modulus Frequency
-----------------------------------------------------------------------------
AR.1 1.1565 -0.0000j 1.1565 -0.0000
AR.2 -0.6635 -1.6660j 1.7933 -0.3103
AR.3 -0.6635 +1.6660j 1.7933 0.3103
-----------------------------------------------------------------------------
0
AutoReg Model Results
==============================================================================
Dep. Variable: consideration No. Observations: 96
Model: AutoReg(3) Log Likelihood -209.518
Method: Conditional MLE S.D. of innovations 2.302
Date: Fri, 10 May 2024 AIC 429.036
Time: 16:05:26 BIC 441.699
Sample: 3 HQIC 434.149
96
====================================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------------
const 3.7217 1.612 2.308 0.021 0.562 6.882
consideration.L1 0.3091 0.104 2.984 0.003 0.106 0.512
consideration.L2 0.3199 0.104 3.075 0.002 0.116 0.524
consideration.L3 0.1620 0.103 1.568 0.117 -0.040 0.364
Roots
=============================================================================
Real Imaginary Modulus Frequency
-----------------------------------------------------------------------------
AR.1 1.1351 -0.0000j 1.1351 -0.0000
AR.2 -1.5553 -1.7380j 2.3322 -0.3662
AR.3 -1.5553 +1.7380j 2.3322 0.3662
-----------------------------------------------------------------------------
AutoReg Model Results
==============================================================================
Dep. Variable: liking No. Observations: 96
Model: AutoReg(3) Log Likelihood 26.938
Method: Conditional MLE S.D. of innovations 0.181
Date: Fri, 10 May 2024 AIC -43.877
Time: 16:05:26 BIC -31.214
Sample: 3 HQIC -38.764
96
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
const 1.2341 0.471 2.619 0.009 0.310 2.158
liking.L1 0.4193 0.100 4.173 0.000 0.222 0.616
liking.L2 0.1165 0.109 1.070 0.285 -0.097 0.330
liking.L3 0.2257 0.100 2.250 0.024 0.029 0.422
Roots
=============================================================================
Real Imaginary Modulus Frequency
-----------------------------------------------------------------------------
AR.1 1.1628 -0.0000j 1.1628 -0.0000
AR.2 -0.8395 -1.7621j 1.9519 -0.3208
AR.3 -0.8395 +1.7621j 1.9519 0.3208
-----------------------------------------------------------------------------
From the results above, we can calculate the stickiness of our mindset metrics by summing their AR coefficients as follows.
stick_awareness = result_ar1.params['awareness.L1'] + result_ar1.params['awareness.L2'] + result_ar1.params['awareness.L3']
stick_consideration = result_ar2.params['consideration.L1'] + result_ar2.params['consideration.L2'] + result_ar2.params['consideration.L3']
stick_liking = result_ar3.params['liking.L1'] + result_ar3.params['liking.L2'] + result_ar3.params['liking.L3']
print(stick_awareness.round(3), stick_consideration.round(3), stick_liking.round(3))
0.767 0.791 0.761
To summarize, the stickiness measures for the awareness, consideration, and liking metrics are 0.767, 0.791 and 0.761, respectively.
Responsiveness
Responsiveness, or lift, is the short-term response of the attitude metric with respect to a marketing stimulus. We recommend using well-established, robust response functions to estimate responsiveness. For example, the standard multiplicative response model produces elasticities as the following responsiveness metrics:
$$ Y_t= cY_{t-1}^\gamma X_{1t}^{\beta_1} X_{2t}^{\beta_2} e^{u_t} $$where Y is an attitude metric and $X_i$ (i=1,2) are marketing instruments. Not only do such response models provide readily interpretable results, but they have also been shown to outperform more complex specifications in forecasting product trials for consumer-packaged goods.
Note that responsiveness may be related to potential as follows: the closer the attitude metric is to its ceiling value, the more difficult it will be to register further increases through marketing. To reflect that in the calculation, once we get the responsiveness of each mindset metric and sales, we can multiply it with the corresponding potential:
$$ \theta_i = \beta_i * Potential_i $$To estimate the responsiveness model, which is multiplicative, an easy way is to take the logarithm of both sides of the equation and estimate the model as a log-linear one. To be specific, we first take the log such that:
$$ log(Y_t)= c'+\gamma log(Y_{t-1}) + {\beta_1} log(X_{1t}) + {\beta_2} log(X_{2t}) + {u_t} $$Now, we examine the log-linear model for each mindset metric and sales to get responsiveness:
#Define the lagged Variables
df['lag_aware'] = df['awareness'].shift(1)
df.loc[0, 'lag_aware'] = 0
df['lag_liking'] = df['liking'].shift(1)
df.loc[0, 'lag_liking'] = 0
df['lag_consideration'] = df['consideration'].shift(1)
df.loc[0, 'lag_consideration'] = 0
df['lag_sales'] = df['vol'].shift(1)
df.loc[0, 'lag_sales'] = 0
# Define the log-linear models
response_aware = sm.OLS(np.log1p(df['awareness']), sm.add_constant(np.log1p(df[['lag_aware', 'price', 'promotion', 'advertising']])))
result_aware = response_aware.fit()
response_consideration = sm.OLS(np.log1p(df['consideration']), sm.add_constant(np.log1p(df[['lag_consideration', 'price', 'promotion', 'advertising']])))
result_consideration = response_consideration.fit()
response_liking = sm.OLS(np.log1p(df['liking']), sm.add_constant(np.log1p(df[['lag_liking', 'price', 'promotion', 'advertising']])))
result_liking = response_liking.fit()
response_sales = sm.OLS(np.log1p(df['vol']), sm.add_constant(np.log1p(df[['lag_sales', 'price', 'promotion', 'advertising']])))
result_sales = response_sales.fit()
# Summarize the regression results
print(result_aware.summary())
print(result_consideration.summary())
print(result_liking.summary())
print(result_sales.summary())
OLS Regression Results
==============================================================================
Dep. Variable: awareness R-squared: 0.427
Model: OLS Adj. R-squared: 0.402
Method: Least Squares F-statistic: 16.97
Date: Fri, 10 May 2024 Prob (F-statistic): 1.98e-10
Time: 16:05:31 Log-Likelihood: 77.882
No. Observations: 96 AIC: -145.8
Df Residuals: 91 BIC: -132.9
Df Model: 4
Covariance Type: nonrobust
===============================================================================
coef std err t P>|t| [0.025 0.975]
-------------------------------------------------------------------------------
const 2.9509 0.554 5.331 0.000 1.851 4.051
lag_aware 0.0467 0.032 1.446 0.152 -0.017 0.111
price -0.0830 0.331 -0.250 0.803 -0.741 0.575
promotion 0.0215 0.015 1.458 0.148 -0.008 0.051
advertising 0.0521 0.008 6.729 0.000 0.037 0.067
==============================================================================
Omnibus: 13.507 Durbin-Watson: 1.448
Prob(Omnibus): 0.001 Jarque-Bera (JB): 14.501
Skew: -0.883 Prob(JB): 0.000710
Kurtosis: 3.710 Cond. No. 444.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
OLS Regression Results
==============================================================================
Dep. Variable: consideration R-squared: 0.341
Model: OLS Adj. R-squared: 0.312
Method: Least Squares F-statistic: 11.79
Date: Fri, 10 May 2024 Prob (F-statistic): 9.33e-08
Time: 16:05:31 Log-Likelihood: 59.041
No. Observations: 96 AIC: -108.1
Df Residuals: 91 BIC: -95.26
Df Model: 4
Covariance Type: nonrobust
=====================================================================================
coef std err t P>|t| [0.025 0.975]
-------------------------------------------------------------------------------------
const 1.8628 0.654 2.846 0.005 0.563 3.163
lag_consideration 0.0996 0.044 2.253 0.027 0.012 0.187
price 0.2801 0.401 0.698 0.487 -0.517 1.077
promotion 0.0662 0.018 3.622 0.000 0.030 0.102
advertising 0.0177 0.009 1.883 0.063 -0.001 0.036
==============================================================================
Omnibus: 1.577 Durbin-Watson: 1.409
Prob(Omnibus): 0.454 Jarque-Bera (JB): 1.071
Skew: -0.229 Prob(JB): 0.585
Kurtosis: 3.239 Cond. No. 425.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
OLS Regression Results
==============================================================================
Dep. Variable: liking R-squared: 0.329
Model: OLS Adj. R-squared: 0.300
Method: Least Squares F-statistic: 11.16
Date: Fri, 10 May 2024 Prob (F-statistic): 2.08e-07
Time: 16:05:31 Log-Likelihood: 195.26
No. Observations: 96 AIC: -380.5
Df Residuals: 91 BIC: -367.7
Df Model: 4
Covariance Type: nonrobust
===============================================================================
coef std err t P>|t| [0.025 0.975]
-------------------------------------------------------------------------------
const 1.4541 0.161 9.037 0.000 1.134 1.774
lag_liking -0.0016 0.018 -0.089 0.929 -0.038 0.035
price 0.1789 0.097 1.843 0.069 -0.014 0.372
promotion 0.0192 0.004 4.434 0.000 0.011 0.028
advertising 0.0024 0.002 1.053 0.295 -0.002 0.007
==============================================================================
Omnibus: 3.893 Durbin-Watson: 1.317
Prob(Omnibus): 0.143 Jarque-Bera (JB): 2.387
Skew: 0.167 Prob(JB): 0.303
Kurtosis: 2.303 Cond. No. 411.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
OLS Regression Results
==============================================================================
Dep. Variable: vol R-squared: 0.590
Model: OLS Adj. R-squared: 0.572
Method: Least Squares F-statistic: 32.73
Date: Fri, 10 May 2024 Prob (F-statistic): 6.73e-17
Time: 16:05:31 Log-Likelihood: -13.027
No. Observations: 96 AIC: 36.05
Df Residuals: 91 BIC: 48.88
Df Model: 4
Covariance Type: nonrobust
===============================================================================
coef std err t P>|t| [0.025 0.975]
-------------------------------------------------------------------------------
const 6.2350 1.411 4.420 0.000 3.433 9.037
lag_sales 0.0642 0.039 1.647 0.103 -0.013 0.142
price -0.2776 0.852 -0.326 0.745 -1.969 1.414
promotion 0.2970 0.041 7.214 0.000 0.215 0.379
advertising 0.0596 0.020 2.989 0.004 0.020 0.099
==============================================================================
Omnibus: 2.721 Durbin-Watson: 0.906
Prob(Omnibus): 0.256 Jarque-Bera (JB): 2.381
Skew: -0.135 Prob(JB): 0.304
Kurtosis: 3.723 Cond. No. 574.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Then, we summarize our estimation results in the below table:
# Create tables of rounded coefficients
table_response_aware = pd.DataFrame(round(result_aware.params, 4), columns=['Estimate'])
table_response_consideration = pd.DataFrame(round(result_consideration.params, 4), columns=['Estimate'])
table_response_liking = pd.DataFrame(round(result_liking.params, 4), columns=['Estimate'])
table_response_sales = pd.DataFrame(round(result_sales.params, 4), columns=['Estimate'])
# Display the tables
print("Table Response Aware:")
print(table_response_aware)
print("\nTable Response Consideration:")
print(table_response_consideration)
print("\nTable Response Consideration:")
print(table_response_consideration)
print("\nTable Response Liking:")
print(table_response_liking)
print("\nTable Response Sales:")
print(table_response_sales)
Table Response Aware:
Estimate
const 2.9509
lag_aware 0.0467
price -0.0830
promotion 0.0215
advertising 0.0521
Table Response Consideration:
Estimate
const 1.8628
lag_consideration 0.0996
price 0.2801
promotion 0.0662
advertising 0.0177
Table Response Consideration:
Estimate
const 1.8628
lag_consideration 0.0996
price 0.2801
promotion 0.0662
advertising 0.0177
Table Response Liking:
Estimate
const 1.4541
lag_liking -0.0016
price 0.1789
promotion 0.0192
advertising 0.0024
Table Response Sales:
Estimate
const 6.2350
lag_sales 0.0642
price -0.2776
promotion 0.2970
advertising 0.0596
Conversion
An attitudinal metric would be meaningless for a brand if it does not impact sales. That is why we need to evaluate its power in terms of changing sales. Conversion is the degree to which movements in the attitudinal metric convert to sales, similar to a conversion rate of leads into customer orders in B2B. To calculate conversion, we again allow for a multiplicative funnel model that can be applied across different conditions.
For example, with intermediate attitudinal metrics awareness ($A_t$), consideration ($C_t$ ) and liking ($L_t$), a multiplicative funnel model for sales revenue ($S_t$) would be:
$$ S_t= cS_{t-1}^\gamma A_{t}^{\beta_1} C_{t}^{\beta_2} L_{t}^{\beta_3} e^{v_t} $$To estimate the conversion model, we again take the log of both sides of the equation above and estimate a linear model.
# Define the log-linear model formula using np.log for awareness, consideration, and liking
formula_conversion = 'np.log1p(vol) ~ np.log1p(lag_sales) + np.log(awareness) + np.log(consideration) + np.log(liking)'
# Fit the OLS model with a constant term
conversion_model = sm.OLS.from_formula(formula=formula_conversion, data=df)
result_conversion = conversion_model.fit()
# Summarize the regression results
print(result_conversion.summary())
OLS Regression Results
==============================================================================
Dep. Variable: np.log1p(vol) R-squared: 0.562
Model: OLS Adj. R-squared: 0.542
Method: Least Squares F-statistic: 29.14
Date: Fri, 10 May 2024 Prob (F-statistic): 1.35e-15
Time: 16:05:45 Log-Likelihood: -16.241
No. Observations: 96 AIC: 42.48
Df Residuals: 91 BIC: 55.30
Df Model: 4
Covariance Type: nonrobust
=========================================================================================
coef std err t P>|t| [0.025 0.975]
-----------------------------------------------------------------------------------------
Intercept -1.8907 1.246 -1.517 0.133 -4.366 0.585
np.log1p(lag_sales) 0.1121 0.038 2.925 0.004 0.036 0.188
np.log(awareness) 0.8010 0.253 3.167 0.002 0.299 1.303
np.log(consideration) 0.4267 0.304 1.405 0.163 -0.177 1.030
np.log(liking) 2.8141 0.997 2.821 0.006 0.833 4.795
==============================================================================
Omnibus: 0.035 Durbin-Watson: 1.458
Prob(Omnibus): 0.983 Jarque-Bera (JB): 0.155
Skew: 0.037 Prob(JB): 0.926
Kurtosis: 2.817 Cond. No. 450.
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Summarizing the conversion results, we obtain:
coefficients = result_conversion.params
table_conversion = pd.DataFrame({'Estimate': coefficients.values}, index=coefficients.index)
table_conversion = table_conversion.round(4)
print(table_conversion)
Estimate Intercept -1.8907 np.log1p(lag_sales) 0.1121 np.log(awareness) 0.8010 np.log(consideration) 0.4267 np.log(liking) 2.8141
Summary of the results
The following code combines all the results from previous states together to gain a clear understanding of the measures on potential, stickiness, responsiveness, and conversion.
import warnings
warnings.filterwarnings("ignore")
mean_awareness = np.mean(df['awareness']) / 100
mean_consideration = np.mean(df['consideration']) / 100
mean_liking = np.mean(df['liking_transformed']) / 100
# Calculate potential for awareness and consideration
potential_awareness = round((1 - mean_awareness), 3)
potential_consideration = round((1 - mean_consideration), 3)
# Calculate potential for liking
df['liking_transformed'] = df['liking'] / 7
potential_liking = round((1 - df['liking_transformed'].mean()), 3)
# Create DataFrame
table_final = pd.DataFrame({
'Item': ["beginning level", "potential", "stickiness", "responsiveness to advertising",
"responsiveness to promotion", "conversion"],
'awareness': [
round(mean_awareness, 3),
potential_awareness,
round(stick_awareness, 3),
round(result_aware.params[4], 3),
round(result_aware.params[3], 3),
round(result_conversion.params[2], 3)
],
'consideration': [
round(mean_consideration, 3),
potential_consideration,
round(stick_consideration, 3),
round(result_consideration.params[4], 3),
round(result_consideration.params[3], 3),
round(result_conversion.params[3], 3)
],
'liking': [
round(mean_liking, 3),
potential_liking,
round(stick_liking, 3),
round(result_liking.params[4], 3),
round(result_liking.params[3], 3),
round(result_conversion.params[4], 3)
]
})
# Display the final table
print(table_final)
Item awareness consideration liking 0 beginning level 0.275 0.175 0.007 1 potential 0.725 0.825 0.263 2 stickiness 0.767 0.791 0.761 3 responsiveness to advertising 0.052 0.018 0.002 4 responsiveness to promotion 0.021 0.066 0.019 5 conversion 0.801 0.427 2.814
3. Appeal
Finally, the appeal of each marketing mix action (i.e. promotion and advertising) is calculated by multiplying the potential, responsiveness, long-run multiplier that uses stickiness,and conversion.
$$ Appeal = Potential * Reponsiveness_{short-run}* 1/(1-Stickiness) * Conversion $$# Assuming 'table_final', 'result_advertising', 'result_promotion', 'result_conversion' are defined
# Calculate appeal for awareness - advertising
appeal_adv_awareness = round(
table_final.loc[1, 'awareness'] * # potential
table_final.loc[3, 'awareness'] * # responsiveness
(1 / (1 - table_final.loc[2, 'awareness'])) * # long-run multiplier using stickiness
table_final.loc[5, 'awareness'], # conversion
3
)
# Calculate appeal for awareness - promotion
appeal_promo_awareness = round(
table_final.loc[1, 'awareness'] * # potential
table_final.loc[4, 'awareness'] * # responsiveness
(1 / (1 - table_final.loc[2, 'awareness'])) * # long-run multiplier using stickiness
table_final.loc[5, 'awareness'], # conversion
3
)
# Calculate appeal for consideration - advertising
appeal_adv_consideration = round(
table_final.loc[1, 'consideration'] * # potential
table_final.loc[3, 'consideration'] * # responsiveness
(1 / (1 - table_final.loc[2, 'consideration'])) * # long-run multiplier using stickiness
table_final.loc[5, 'consideration'], # conversion
3
)
# Calculate appeal for consideration - promotion
appeal_promo_consideration = round(
table_final.loc[1, 'consideration'] * # potential
table_final.loc[4, 'consideration'] * # responsiveness
(1 / (1 - table_final.loc[2, 'consideration'])) * # long-run multiplier using stickiness
table_final.loc[5, 'consideration'], # conversion
3
)
# Calculate appeal for liking - advertising
appeal_adv_liking = round(
table_final.loc[1, 'liking'] * # potential
table_final.loc[3, 'liking'] * # responsiveness
(1 / (1 - table_final.loc[2, 'liking'])) * # long-run multiplier using stickiness
table_final.loc[5, 'liking'], # conversion
3
)
# Calculate appeal for liking - promotion
appeal_promo_liking = round(
table_final.loc[1, 'liking'] * # potential
table_final.loc[4, 'liking'] * # responsiveness
(1 / (1 - table_final.loc[2, 'liking'])) * # long-run multiplier using stickiness
table_final.loc[5, 'liking'], # conversion
3
)
# Create DataFrame for appeal
appeal_table = pd.DataFrame({
'Item': ["appeal_advertising", "appeal_promotion"],
'awareness': [appeal_adv_awareness, appeal_promo_awareness],
'consideration': [appeal_adv_consideration, appeal_promo_consideration],
'liking': [appeal_adv_liking, appeal_promo_liking]
})
# Display the appeal table
print(appeal_table)
Item awareness consideration liking 0 appeal_advertising 0.130 0.030 0.006 1 appeal_promotion 0.052 0.111 0.059
Question: What is your takeaway from these results? Which marketing tool (i.e. promotion or advertising) is more effective for the marketing manager of Zanten to improve consumer awareness, consideration and liking? If you were the manager of this shampoo brand, what type of marketing strategy would you like to adopt in general?
4. What-if analysis
Given that this chapter focuses on the value of marketing activities through consumer mindset metrics on brand sales, we would like to evaluate the impact of change in a particular marketing tool on long-term sales and decompose such effect into gain/loss due to mindset metrics and gain/loss due to transactions.
In particular, we will evaluate two alternative options for Zhang, the marketing manager of Zanten:
Quintupling advertising spending
Doubling promotion
Quintupling advertising spending
Suppose that Zhang decides to quintuple advertising spending from \£100K to \£500K while keeping the rest of the marketing mix decisions same.
Let us visualise the calculations by using the below table:
mean_awareness = np.mean(df['awareness']) / 100
mean_consideration = np.mean(df['consideration']) / 100
mean_liking = np.mean(df['liking_transformed'])
mean_sales = np.mean(df['vol'])
# Creating the DataFrame
table_whatif = pd.DataFrame({
'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
'Start': [100, 100, round(mean_awareness, 3), round(mean_consideration, 3), round(mean_liking, 3), round(mean_sales, 3)],
'New': [500, 100, "?", "?", "?", "?"],
'Gain': [400, 0, "?", "?", "?", "?"],
'LRGain': ["/", "/", "?", "?", "?", "?"],
'Contribution': ["/", "/", "?", "?", "?", "/"]
})
# Print the DataFrame
print(table_whatif)
Item Start New Gain LRGain Contribution 0 Advertising 100.000 500 400 / / 1 Promotion 100.000 100 0 / / 2 Awareness 0.275 ? ? ? ? 3 Consideration 0.175 ? ? ? ? 4 Liking 0.737 ? ? ? ? 5 Sales 1800.391 ? ? ? /
The cells with question marks are the ones that we need to fill in. We proceed as follows:
To briefly summarise, we need to do the following:
First, we derive the new level of each mindset metric and sales based on the changes that we decide to make to the marketing mix decisions (i.e. advertising).
Then, by comparing the new and the old levels, we can derive immediate or short-run gain for each metric and sales.
Next, with the help of carryover parameters from responsiveness models, we transform those short-run gains into corresponding long-run gains.
Finally, to decompose the long-run gain in sales and attribute it to gains due to mindset metrics and gains due to transaction, we calculate how much the long-run gains from step 3 translate into an actual increase in sales.
New values
From the table, the column ‘Start’ summarizes all the base values for each item. For example, the average level of awareness, expressed in percentage terms, is 27.5 per cent, leaving a 72.5 per cent potential.
Now, we need to decide what happens to all the mindset metrics and sales after we quintuple advertising (from 100K to 500K). First, we need to determine the new level of each metric and sale after the change to the marketing mix. More specifically, the change to marketing mix exerts its impact on each metric through short-run responsiveness. Therefore, we need to go back to our previous analysis and use the model parameters in responsiveness models. For mindset metric $M_i$ (i=1,2,3 in our case), we apply the formula below:
$$ New_{M_i} = Start_{M_i} *(Advertising_{new}/Advertising_{old})^{Responsiveness_{Advertising_{M_i}}}) $$# Calculate new awareness
new_awareness = round((1 - table_potential.iloc[0, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'awareness'], 3)
# Calculate new consideration
new_consideration = round((1 - table_potential.iloc[1, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'consideration'], 3)
# Calculate new liking
new_liking = round((1 - table_potential.iloc[2, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'liking'], 3)
# Calculate new sales
new_sales = round(np.mean(df['vol']) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** float(result_sales.params[4]), 3)
Short-run gain
We derive the gain (loss if negative) in each mindset metric and sales by simply calculating the growth rate. The code below calculates the short-run gains.
# Calculate gain (from "start" to "new")
gain_awareness = round((new_awareness / (1 - table_potential.iloc[0, 1])) - 1, 3)
gain_consideration = round((new_consideration / (1 - table_potential.iloc[1, 1])) - 1, 3)
gain_liking = round((new_liking / (1 - table_potential.iloc[2, 1])) - 1, 3)
gain_sales = round((new_sales / np.mean(df['vol'])) - 1, 3)
Long-run gain
Now, we can translate the short-run gain to long-run gain for each metric and sales performance. To do this, we need to make use of the carryover (gamma) we get from responsiveness models (you may want to go back and check the tables named ‘table_responsiveness’). Again, for each mindset metric $M_i$ and sales:
$$ LRGain_{M_i} = Gain_{M_i}/(1-Carryover_{M_i}) $$warnings.filterwarnings("ignore")
#Calculate long run gain for each mindset metric and sales
lrgain_awareness = round(gain_awareness / (1 - float(result_aware.params[1])), 3)
lrgain_consideration = round(gain_consideration / (1 - float(result_consideration.params[1])), 3)
lrgain_liking = round(gain_liking / (1 - float(result_liking.params[1])), 3)
lrgain_sales = round(gain_sales / (1 - float(result_sales.params[1])), 3)
Contribution
In the final step, we need to go back and refer to the conversion model (you may want to go back and check the table named ‘table_conversion’):
$$ Contribution_{M_i} = LRGain_{M_i} * Conversion_{M_i} $$#Calculate conversion, i.e., decompose the gain and attribute to awareness, consideration, and liking
contribution_awareness = round(lrgain_awareness * table_final.loc[5, "awareness"], 3)
contribution_consideration = round(lrgain_consideration * table_final.loc[5, "consideration"], 3)
contribution_liking = round(lrgain_liking * table_final.loc[5, "liking"], 3)
Final Output
Finally, we are able to fill in all the empty cells:
table_whatif = pd.DataFrame({
'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
'Start': [round(val, 3) for val in [100, 100, np.mean(df['awareness']) / 100, np.mean(df['consideration']) / 100, np.mean(df['liking_transformed']), np.mean(df['vol'])]],
'New': [round(val, 3) for val in [500, 100, new_awareness, new_consideration, new_liking, new_sales]],
'Gain': [round(val, 3) for val in [400, 0, gain_awareness, gain_consideration, gain_liking, gain_sales]],
'LRGain': ["/", "/", lrgain_awareness, round(lrgain_consideration, 3), round(lrgain_liking, 3), round(lrgain_sales, 3)],
'Contribution': ["/", "/", round(contribution_awareness, 3), round(contribution_consideration, 3), round(contribution_liking, 3), "/"]
})
# Print the DataFrame
print(table_whatif)
Item Start New Gain LRGain Contribution 0 Advertising 100.000 500.000 400.000 / / 1 Promotion 100.000 100.000 0.000 / / 2 Awareness 0.275 0.299 0.088 0.092 0.074 3 Consideration 0.175 0.180 0.030 0.033 0.014 4 Liking 0.737 0.739 0.003 0.003 0.008 5 Sales 1800.391 1981.787 0.101 0.108 /
By summing up $Contribution_{awareness}$, $Contribution_{consideration}$, and $Contribution_{liking}$, we can get the total long run gain in sales that originates from improvement in mindset metrics.
- We have: 0.074+0.014+0.008 = 0.096
We observe that:
Long term sales gain = 10.8%
Sales gain due to mindset metrics = 9.6%
Sales gain due to transactions = 10.8% - 9.6% = 1.2%
Therefore, we can tell that by quintupling advertising, there is a significant improvement in mindset metrics, and such improvement can contribute to a 8.6 per cent increase in sales. The eventual net effect on long-run sales is a 10.8 per cent increase, and this is due to a 1.2 per cent increase in sales due to transactions.
Next, we will examine the scenario in which the brand manager of Zanten, Zhang, doubles the promotion spending.
Doubling promotion spending
Suppose that the marketing manager of Zanten doubles the promotion spending from \£100K to \£200K and does not make further changes in the marketing mix. What would be the impact on long-term sales?
Let us visualise the calculations that we need to make by using the table below:
table_whatif2 = pd.DataFrame({
'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
'Start': [100, 100, round(np.mean(df['awareness']) / 100, 3), round(np.mean(df['consideration']) / 100, 3), round(np.mean(df['liking']) / 7, 3), round(np.mean(df['vol']), 3)],
'New': [100, 200, "?", "?", "?", "?"],
'Gain': [0, 100, "?", "?", "?", "?"],
'LRGain': ["/", "/", "?", "?", "?", "?"],
'Contribution': ["/", "/", "?", "?", "?", "/"]
})
# Print the DataFrame
print(table_whatif2)
Item Start New Gain LRGain Contribution 0 Advertising 100.000 100 0 / / 1 Promotion 100.000 200 100 / / 2 Awareness 0.275 ? ? ? ? 3 Consideration 0.175 ? ? ? ? 4 Liking 0.737 ? ? ? ? 5 Sales 1800.391 ? ? ? /
The cells with question marks are the ones we need to fill in. We proceed as follows:
Calculate the new level of each mindset metric and sales based on the changes that we decide to make to the marketing mix (i.e. doubling promotion spending).
Then, by comparing the new and the old levels, we will calculate the short-run gain for each mindset metric and sales.
Next, with the help of carryover parameters from responsiveness models, we will transform those short-run gains into corresponding long-run gains.
Finally, we will decompose the long-run sales gain into transaction and mindset routes.
New
The column 'Start' from the above table summarizes the base values for each metric. For example, the average level of awareness, expressed in percentage terms, is 21.51%, leaving a 78.49% potential.
Next, we will see what happens to all the mindset metrics and sales after we quintuple advertising. To this end, we will determine the new level of each metric and sales after the change to the marketing mix variables (i.e.promotion). We need to use the estimated parameters from the responsiveness models. For mindset metric $M_i$ (i=1,2,3 in our case), we apply the formula below:
$$ New_{M_i} = Start_{M_i} *(Promotion_{new}/Promotion_{old})^{Responsiveness_{Promo_{M_i}}}) $$table_whatif2['Start'] = pd.to_numeric(table_whatif2['Start'], errors='coerce')
table_whatif2['New'] = pd.to_numeric(table_whatif2['New'], errors='coerce')
table_whatif2['Gain'] = pd.to_numeric(table_whatif2['Gain'], errors='coerce')
table_whatif2['LRGain'] = pd.to_numeric(table_whatif2['LRGain'], errors='coerce')
table_whatif2['Contribution'] = pd.to_numeric(table_whatif2['Contribution'], errors='coerce')
#Note that we do not include short run impact of change in ads on each mindset metric and sales because there is no change (100/100=1)
new_awareness2 = round((1 - table_potential.iloc[0, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'awareness'], 3)
new_consideration2 = round((1 - table_potential.iloc[1, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'consideration'], 3)
new_liking2 = round((1 - table_potential.iloc[2, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'liking'], 3)
# Calculate new sales
new_sales2 = round(np.mean(df['vol']) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** float(result_sales.params[3]), 3)
#### Short-run gain
# Calculate gain (from "start" to "new")
gain_awareness2 = round((new_awareness2 / (1 - table_potential.iloc[0, 1])) - 1, 3)
gain_consideration2 = round((new_consideration2 / (1 - table_potential.iloc[1, 1])) - 1, 3)
gain_liking2 = round((new_liking2 / (1 - table_potential.iloc[2, 1])) - 1, 3)
# Assuming result_conversion is the result of your regression
gain_sales2 = round((new_sales2 / np.mean(df['vol'])) - 1, 3)
Long-run gain
We can translate the short run gain into long run gain for each mindset metric and sales. To do this, we need to make use of the stickiness (carryover) parameters we get from the responsiveness models. You may want to go back and check the table named 'table_responsiveness'. So, for each metric ($M_i$), we calculate the long-run gain as follows:
$$ LRGain_{M_i} = Gain_{M_i}/(1-Carryover_{M_i}) $$warnings.filterwarnings("ignore")
# Calculate long run gain for each mindset metric and sales
lrgain_awareness2 = round(gain_awareness2 / (1 - float(result_aware.params[1])), 3)
lrgain_consideration2 = round(gain_consideration2 / (1 - float(result_consideration.params[1])), 3)
lrgain_liking2 = round(gain_liking2 / (1 - float(result_liking.params[1])), 3)
# Assuming result_conversion is the result of your sales regression
lrgain_sales2 = round(gain_sales2 / (1 - float(result_conversion.params[1])), 3)
Contribution
In the final step, we refer to the conversion model. You may want to go back and check the table named 'table_conversion'. To find the contribution, we multiply the long run gain by the conversion parameter:
$$ Contribution_{M_i} = LRGain_{M_i} * Conversion_{M_i} $$# Calculate conversion, i.e., decompose the gain and attribute to awareness, consideration, and liking
contribution_awareness2 = round(lrgain_awareness2 * table_final.loc[5, "awareness"], 3)
contribution_consideration2 = round(lrgain_consideration2 * table_final.loc[5, "consideration"], 3)
contribution_liking2 = round(lrgain_liking2 * table_final.loc[5, "liking"], 3)
Final output
Finally, we are able to fill in all the blank cells:
table_whatif2 = pd.DataFrame({
'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
'Start': [round(x, 3) for x in [100, 100, np.mean(df['awareness']) / 100, np.mean(df['consideration']) / 100, np.mean(df['liking_transformed']) / 7, np.mean(df['vol'])]],
'New': [round(x, 3) for x in [100, 200, new_awareness2, new_consideration2, new_liking2, new_sales2]],
'Gain': [round(x, 3) for x in [0, 100, gain_awareness2, gain_consideration2, gain_liking2, gain_sales2]],
'LRGain': ["/", "/", lrgain_awareness2, round(lrgain_consideration2, 3), round(lrgain_liking2, 3), round(lrgain_sales2, 3)],
'Contribution': ["/", "/", round(contribution_awareness2, 3), round(contribution_consideration2, 3), round(contribution_liking2, 3), "/"]
})
# Print the DataFrame
print(table_whatif2)
Item Start New Gain LRGain Contribution 0 Advertising 100.000 100.000 0.000 / / 1 Promotion 100.000 200.000 100.000 / / 2 Awareness 0.275 0.279 0.016 0.017 0.014 3 Consideration 0.175 0.183 0.047 0.052 0.022 4 Liking 0.105 0.747 0.014 0.014 0.039 5 Sales 1800.391 2211.931 0.229 0.258 /
By summing up $Contribution_{awareness}$, $Contribution_{consideration}$, and $Contribution_{liking}$, we can get the total long run gain in sales that originates from improvement in mindset metrics.
- We have: 0.014 + 0.022 + 0.039 = 0.075
Given that there is a 25.8% gain in sales, we can tell that:
- Long term sales gain = 25.8%
- Sales gain due to mindset metrics = 7.5%
- Sales gain due to transactions = 25.8%-7.5% = 18.3%
Recall that quintupling advertising expense resulted in a 10.8% increase in brand sales. The above analysis shows that doubling promotion results in a 25.8% increase in sales.Therefore, we can conclude that doubling promotion seems to be more effective in driving up sales. However, doubling promotion generates less improvement in consumer mindset metrics. If the ultimate objective of the marketing manager is to 'build a brand', then gains in mindset metrics should be more important. Therefore, depending on the strategic objective of the brand, Zhang, the marketing manager of Zanten, may want to choose either of these two strategies.
To summarize, marketing managers can conduct such scenario analyses, compare the estimated long-run effects, and make evidence-based decisions to win their customers' hearts and minds and boost sales.
5. Conclusion
In this tutorial, we discuss the role of customer mindset metrics – awareness, consideration, and liking – in guiding marketing mix decisions. These metrics are important intermediary performance indicators and allow brand managers to establish an actionable connection between marketing activity and financial performance. We conclude that the relative importance of customer mindset metrics is determined by four criteria: potential, stickiness, responsiveness, and conversion. Using these criteria and harnessing the data on customer mindset metrics, brand managers can assess the investment appeal of their marketing mix decisions and make evidence-based decisions.
Our case study on a fast-moving consumer goods brand, along with its Python application, demonstrates how brand managers can use analytic tools to develop actionable guidelines. These tools help improve marketing mix decisions by leveraging the diagnostic value of customer mindset metrics.